package mc.support.web.service;

import mc.support.web.WebConstants;
import mc.support.web.exception.InvalidParamTypeException;
import mc.support.web.exception.ParamLostExcepion;
import mc.support.web.exception.ServiceException;
import com.esotericsoftware.reflectasm.MethodAccess;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
 * Created with IntelliJ IDEA. <br/>
 * Author: francis Yun    <br/>
 * Date: 2013-11-29  <br/>
 */
public class ServiceRouterImpl implements IServiceRouter, WebConstants {

    private static final byte[] SHARED_LOCK = new byte[0];

    private static final boolean DEFAULT_SERV_CACHE_USAGE = true;

    private static final SimpleDateFormat DEFAULT_DATE_FORMAT = new SimpleDateFormat( GLOBAL_DATE_FORMAT );

    private static final Map<String, ParamResolver> globalParamResolvers = new ConcurrentHashMap<String, ParamResolver>();

    private static final Map<ServiceMeta, Map<String, ServiceAccess>> globalAccessCache =
            new ConcurrentHashMap<ServiceMeta, Map<String, ServiceAccess>>();

    private final Map<ServiceMeta, Map<String, ServiceAccess>> accessCache =
            new ConcurrentHashMap<ServiceMeta, Map<String, ServiceAccess>>();

    // since "getClass" is expensive on some jvm and
    // the fact that service is singleton, we can build a type cache
    private Map<Object, ServiceMeta> typeCache = new ConcurrentHashMap<Object, ServiceMeta>();

    private Map<String, ParamResolver> paramResolvers = new ConcurrentHashMap<String, ParamResolver>();

    private boolean useGlobalServCache = DEFAULT_SERV_CACHE_USAGE;

    @Override
    public Object route( Class<?> interfaceType, Object service, Map<String, Object> params) {
        ServiceMeta meta = typeCache.get( service );
        if ( null == meta ) {
            synchronized ( service ) {
                meta = typeCache.get( service );
                if ( null == meta ) {
                    meta = new ServiceMeta( interfaceType, service.getClass(), "" );
                    typeCache.put( service, meta );
                }
            }
        }
        return route( meta, service, params);
    }

    @Override
    public Object route(Object service, Map<String, Object> params) {
        ServiceMeta meta = typeCache.get(service);
        if ( null == meta ) {
            synchronized ( service ) {
                meta = typeCache.get( service );
                if ( null == meta ) {
                    meta = buildServiceMeta( service );
                    typeCache.put( service, meta );
                }
            }
        }
        return route( meta, service, params);
    }

    @Override
    public void registerParamResolver(String paramKey, ParamResolver paramResolver) {
        if ( null != paramKey && null != paramResolver ) {
            paramResolvers.put( paramKey, paramResolver );
        }
    }

    @Override
    public ParamResolver unregisterParamResolver(String paramKey) {
        if ( null != paramKey ) {
            return paramResolvers.remove(paramKey);
        }
        return null;
    }

    @Override
    public void registerGlobalParamResolver(String paramKey, ParamResolver paramResolver) {
        if ( null != paramKey && null != paramResolver ) {
            globalParamResolvers.put(paramKey, paramResolver);
        }
    }

    @Override
    public ParamResolver unregisterGlobalParamResolver(String paramKey) {
        if ( null != paramKey ) {
            return globalParamResolvers.remove( paramKey );
        }
        return null;
    }

    @Override
    public void enableGlobalServiceCache(boolean enable) {
        this.useGlobalServCache = enable;
    }

    private ParamResolver getParamResolver(String paramKey) {
        ParamResolver paramResolver = paramResolvers.get( paramKey );
        return null != paramResolver ? paramResolver : globalParamResolvers.get( paramKey );
    }

    private ServiceMeta buildServiceMeta(Object instance) {
        Class<?> instanceType = instance.getClass();
        Class<?>[] interfaceTypes = instanceType.getInterfaces();

        Class<?> interfaceType = null;
        String tag = null;

        for ( Class<?> clazz : interfaceTypes ) {
            ServiceTag serviceTag = clazz.getAnnotation( ServiceTag.class );
            if ( null != serviceTag ) {
                interfaceType = clazz;
                tag = serviceTag.value();
                break;
            }
        }

        if ( null == interfaceType ) {
            throw new ServiceException( "No service tag found from : " + interfaceType.getName() );
        }

        return new ServiceMeta( interfaceType, instanceType, tag );
    }

    private Object route( ServiceMeta serviceMeta, Object service, Map<String, Object> params) {
        if ( null == params ) {
            throw new ParamLostExcepion( "No parameter found!" );
        }

        Object reqMethodObj = params.get(REQ_METHOD);
        if ( null == reqMethodObj ) {
            throw new ParamLostExcepion( "No request method found!" );
        }
        if ( !(reqMethodObj instanceof String) ) {
            throw new InvalidParamTypeException( REQ_METHOD + " must be String type!" );
        }
        String reqMethod = (String) reqMethodObj;

        Map<String, ServiceAccess> serviceAccesses = getServiceAccesses( serviceMeta );
        if ( null == serviceAccesses ) {
            throw new ServiceException( "No available service found from type: "
                    + serviceMeta.getInterfaceType().getName() );
        }

        ServiceAccess serviceAccess = serviceAccesses.get( reqMethod );
        if ( null == serviceAccess ) {
            throw new ServiceException( "No available service found for route key: " + reqMethod );
        }

        return serviceAccess.call( service, params );
    }

    private Map<String, ServiceAccess> getServiceAccesses( ServiceMeta servMeta ) {
        Map<String, ServiceAccess> serviceAccesses = accessCache.get(servMeta);
        if ( null == serviceAccesses ) {
            synchronized ( SHARED_LOCK ) {
                serviceAccesses = accessCache.get(servMeta);
                if ( null == serviceAccesses ) {

                    if ( useGlobalServCache ) {
                        serviceAccesses = getServAccessesFromGlobalCache(servMeta);
                    } else {
                        serviceAccesses = buildServiceAccesses( servMeta );
                    }

                    if ( null != serviceAccesses ) {
                        accessCache.put(servMeta, serviceAccesses);
                    }

                }
            }
        }

        return serviceAccesses;
    }

    private Map<String, ServiceAccess> getServAccessesFromGlobalCache(ServiceMeta servMeta) {
        Map<String, ServiceAccess> serviceAccesses = globalAccessCache.get( servMeta );
        if ( null == serviceAccesses ) {
            serviceAccesses = buildServiceAccesses( servMeta );
            if ( null != serviceAccesses ) {
                globalAccessCache.put( servMeta, serviceAccesses );
            } else {
                throw new ServiceException( "No available route to service: " + servMeta.getInterfaceType().getName() );
            }
        }
        return serviceAccesses;
    }

    private Map<String, ServiceAccess> buildServiceAccesses( ServiceMeta servMeta ) {
        Class<?> interfaceType = servMeta.getInterfaceType();
        Class<?> instanceType = servMeta.getInstanceType();

        //meta information is tagged at interface
        Method[] methods = interfaceType.getDeclaredMethods();
        Map<String, ServiceAccess> serviceAccesses = null;
        for ( Method method : methods ) {
            if ( Modifier.isPublic( method.getModifiers() ) ) {
                Route route = method.getAnnotation( Route.class );
                if ( null != route ) {

                    String[] serviceKeys = route.accept();
                    if ( null == serviceKeys || 0 == serviceKeys.length ) {
                        throw new ServiceException( "No acceptable service key found!" );
                    }

                    if ( null == serviceAccesses ) {
                        serviceAccesses = new ConcurrentHashMap<String, ServiceAccess>();
                    }

                    MethodAccess methodAccess = MethodAccess.get( instanceType );
                    ParamMeta[] paramMetas = buildParamMeta( method );
                    int methodIndex = methodAccess.getIndex( method.getName(), method.getParameterTypes() );
                    ServiceAccess serviceAccess = new ServiceAccess( methodAccess, methodIndex, paramMetas );

                    for ( String serviceKey : serviceKeys ) {
                        serviceAccesses.put( serviceKey, serviceAccess );
                    }

                }
            }
        }

        return serviceAccesses;
    }

    private ParamMeta[] buildParamMeta(Method method) {
        Annotation[][] AllAnnotations = method.getParameterAnnotations();
        Class<?>[] parameterTypes = method.getParameterTypes();

        int paramCount = parameterTypes.length;
        ParamMeta[] paramMetas = new ParamMeta[ paramCount ];
        for ( int i = 0; i < paramCount; i++ ) {
            Annotation[] annotations = AllAnnotations[i];
            ParamKey paramKey = null;
            for ( Annotation annotation : annotations ) {
                if ( ParamKey.class.equals( annotation.annotationType() ) ) {
                    paramKey = ParamKey.class.cast( annotation );
                    break;
                }
            }

            if ( null == paramKey ) {
                throw new ServiceException( "Method: " + method.getName() +
                        " lack of annotation:" + ParamKey.class.getName() );
            }

            String literal = paramKey.name();
            Class<?> paramType = parameterTypes[i];
            Require require = paramKey.require();

            paramMetas[i] = new ParamMeta( literal, paramType, require);
        }

        return paramMetas;
    }

    private static class ServiceAccess {

        private final MethodAccess access;
        private final int methodIndex;
        private final ParamMeta[] paramMetas;

        private ServiceAccess(MethodAccess access, int methodIndex, ParamMeta[] paramMetas) {
            this.access = access;
            this.methodIndex = methodIndex;
            this.paramMetas = paramMetas;
        }

        public Object call(Object service, Map<String, Object> params) {
            Object[] parameters = new Object[ paramMetas.length ];
            for ( int i = 0; i < parameters.length; i++ ) {
                parameters[i] = paramMetas[i].resolveValue( params );
            }

            return access.invoke( service, methodIndex ,parameters );
        }

    }

    private static class ServiceMeta {

        private final Class<?> interfaceType;
        private final Class<?> instanceType;
        private final String serviceTag;

        private ServiceMeta(Class<?> interfaceType, Class<?> instanceType, String serviceTag) {
            this.interfaceType = interfaceType;
            this.instanceType = instanceType;
            this.serviceTag = serviceTag;
        }

        private Class<?> getInterfaceType() {
            return interfaceType;
        }

        private Class<?> getInstanceType() {
            return instanceType;
        }

        private String getServiceTag() {
            return serviceTag;
        }

        @Override
        public int hashCode() {
            int high16 = instanceType.hashCode();
            int low16 = instanceType.hashCode();
            return ( ( high16 << 16 ) & 0xffff0000 )
                    | ( ( low16 ) & 0x0000ffff );
        }

        @Override
        public boolean equals(Object obj) {
            if ( null != obj && obj instanceof ServiceMeta ) {
                ServiceMeta that = (ServiceMeta) obj;
                return this.instanceType.equals( that.instanceType ) &&
                        this.interfaceType.equals( that.interfaceType );
            }
            return super.equals(obj);
        }
    }

    private class ParamMeta {
        private final String paramKey;
        private final Class<?> type;
        private final Require require;

        private ParamMeta(String paramKey, Class<?> type, Require require) {
            this.paramKey = paramKey;
            this.type = type;
            this.require = require;
        }

        public Object resolveValue(Map<String, Object> params) throws ParamLostExcepion {
            Object raw = params.get( paramKey );
            if ( null != raw ) {
                if ( raw instanceof String ) {
                    raw = resolveByLiteral((String) raw, type);
                } else {
                    if ( type.isInstance( raw ) ) {
                        raw = type.cast( raw );
                    } else {
                        Class<?> receivedType = raw.getClass();
                        if ( type.isAssignableFrom( receivedType ) ) {
                            raw = type.cast( raw );
                        } else {
                            throw new InvalidParamTypeException( "type of parameter " + paramKey +
                                    " must be " + type.getName() );
                        }
                    }
                }
            } else {
                if ( Require.must == require ) {
                    throw new ParamLostExcepion( "can't find parameter: " + paramKey );
                }
            }
            return raw;
        }

        //speed up some type casting
        protected Object resolveByLiteral(String raw, Class<?> type) {
            if ( String.class.equals( type ) ) {
                return (String) raw;
            }
            if ( Integer.TYPE.equals( type ) || Integer.class.equals( type ) ) {
                return isNumeric( raw ) ? Integer.valueOf( raw ) : null;
            }
            if ( Long.TYPE.equals( type ) || Long.class.equals( type ) ) {
                return isNumeric( raw ) ? Long.valueOf( raw ) : null;
            }
            if ( Boolean.TYPE.equals( type ) || Boolean.class.equals( type  ) ) {
                return Boolean.valueOf( raw );
            }
            if ( Byte.TYPE.equals( type ) || Byte.class.equals( type ) ) {
                return isNumeric( raw ) ? Byte.valueOf( raw ) : null;
            }
            if ( Short.TYPE.equals( type ) || Short.class.equals( type ) ) {
                return isNumeric( raw ) ? Short.valueOf( raw ) : null;
            }
            if ( Float.TYPE.equals( type ) || Float.class.equals( type ) ) {
                return isDecimal( raw ) ? Float.valueOf( raw ) : null;
            }
            if ( Double.TYPE.equals( type ) || Double.class.equals( type ) ) {
                return isDecimal( raw ) ? Double.valueOf( raw ) : null;
            }

            if ( Date.class.equals( type ) ) {
                try {
                    return DEFAULT_DATE_FORMAT.parse( raw );
                } catch (ParseException e) {
                    throw new InvalidParamTypeException( "unaccepted date format: " + raw );
                }
            }

            ParamResolver paramResolver = getParamResolver( paramKey );
            if ( null != paramResolver ) {
                Object value = null;
                try {
                    value = paramResolver.resolve( raw );
                } catch (Exception e) {
                    throw new InvalidParamTypeException( "ParamResolver with paramKey : " + paramKey +
                            " resolve value fail! ", e );
                }
                return value;
            }

            throw new InvalidParamTypeException( "can't resolve value for type: " + type.getName() +
            " from literal: " + raw );
        }


        private boolean isNumeric(String str) {
            int sz = 0;
            if ( str != null && ( sz = str.length() ) > 0 ) {
                for ( int i = 0; i < sz; i++ ) {
                    if ( !Character.isDigit( str.charAt(i) ) ) {
                        break;
                    }
                }
                return true;
            }

            throw new InvalidParamTypeException("invalid integer number literal!");
        }

        private boolean isDecimal(String str) {
            if( str != null && str.length() > 0 ) {
                Pattern pattern = Pattern.compile("[0-9]*(\\.?)[0-9]*");
                return pattern.matcher( str ).matches();
            }

            throw new InvalidParamTypeException("invalid float number literal!");
        }

    }

}
